package com.example.redis_redisson_demo.web.redis;

import com.example.redis_redisson_demo.common.utils.R;
import com.example.redis_redisson_demo.dao.AttrDao;
import com.example.redis_redisson_demo.entity.AttrEntity;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

/**
 * 使用StringRedisTemplate实现redis事务
 */
@RestController
@RequestMapping("/redistrans")
public class RedisTransController {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    AttrDao attrDao;

    /**
     * redis自身支持简单的事务，但是是一种非常低级的事务
     * redis事务只保证同一个事务内的所有命令会在全部接收后再执行
     * 但redis事务不具备回滚，也不具备Mysql事务的隔离性
     *
     * @return
     */
    @RequestMapping("/multitrans")
    public R multiTrans(){
        String redisKeyPrex = "demo:redis:";
        AttrEntity attrEntity = attrDao.selectById(2L);
        R r = R.ok();

        stringRedisTemplate.opsForValue().set(redisKeyPrex + "1", "transValue");


        /**
         * public void multi()
         * 使用StringRedisTemplate.multi开启redis事务
         */
        stringRedisTemplate.multi();
        /**
         * 在multi-exec之间的命令将不会被执行，而是发送到redis之后暂存在redis上
         * 当收到exec后，会一次性执行暂存的redis
         * P.S.这些命令在暂存阶段不会得到任何的检查。直到执行时才会。
         * 但是在执行时出现的错误(例如在非Integer数据上运用increment)，只会停止之后的语句，已经执行完成的语句无法进行回滚
         */
        stringRedisTemplate.opsForValue().set(redisKeyPrex + "1", "transNewValue");
        /**
         * public void discard()
         * 使用StringRedisTemplate.discard在一个multi-exec中手动停止事务
         * 当使用该命令时，已经发送到redis暂存的命令将不会被执行，后端直接跳到exec执行之后的内容。
         */
        stringRedisTemplate.discard();
        stringRedisTemplate.opsForValue().set(redisKeyPrex + "1", "transAnotherValue");
        /**
         * public void exec()
         * 使用StringRedisTemplate.exec关闭redis事务
         */
        stringRedisTemplate.exec();

        /**
         * void watch(K key) / void watch(Collection<K> keys)
         * 对一个或多个k进行监视，需要和 multi-exec组合使用
         * 如果在任何一个multi-exec之间 监视的K-V发生变化(例如被其他链接修改)，则自动丢弃当前multi-exec之间的全部内容
         */
        stringRedisTemplate.watch(redisKeyPrex + "1");

        /**
         * 注，直接使用StringRedisTemplate的multi和exec貌似存在Bug，无法执行事务
         * 选择使用 StringRedisTemplate.execute(SessionCallback)
         */
        stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                operations.watch(redisKeyPrex + "101");
                operations.multi();
                // 搞了一个延迟模拟某个客户端占用时间很长
                // 此时再开一个网页访问redis别的请求，依然正常，StringRedisTemplate会使用一个新的链接
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                operations.opsForValue().set(redisKeyPrex + "101", "execute");
                return operations.exec();
            }
        });
        return r;
    }


    /**
     * redis支持使用Lua脚本来保证原子性执行任务
     * Lua脚本本身自带很多redis接口
     *
     * @return
     */
    @RequestMapping("/luatrans")
    public R luaTrans(){
        String redisKeyPrex = "demo:redis:lua";
        AttrEntity attrEntity = attrDao.selectById(2L);
        R r = R.ok();

        stringRedisTemplate.opsForValue().set(redisKeyPrex + "1", "newValue");

        /**
         * 编写一个Lua脚本
         * 在Java中以String类型编写Lua脚本，因此双引号需要用转义字符
         * Lua脚本中所有的redis键都用 KEYS[i] 来表示
         * 其他的输入参数使用 ARGV[i] 来表示
         *
         * 下面这个Lua脚本实现原子删除
         *
         * Lua脚本实现的各种原子操作，可以使用 redisson 实现
         */
        String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";

        // 使用StringRedisTemplate.execute调用Lua脚本
        // 第一个参数为调用的Lua脚本变量
        // 第二个参数为List形式的所有的Keys，第三个之后的参数为所有的ARGV
        Integer delate = stringRedisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Integer.class),
                Arrays.asList(redisKeyPrex + "1"),
                "newValue");

        return r;
    }
}
